﻿/*
 * Copyright © 2020, Wuxi Safover Intelligent Technology Co., Ltd. All rights reserved.
 * 
 * 无锡安永智能科技有限公司版权所有，未经许可，不得用于商业用途
 * 
 * Visit: https://www.safover.com/ 
 */

using System;
using System.Net.Sockets;
using System.Net;
using System.ComponentModel;
using System.Diagnostics;
using System.Text;
using System.Threading;
using System.Collections.Generic;
using System.Reflection;
using log4net;
using Safover.LidarCollector.Core;
using Safover.LidarCollector.Extensions;

namespace Ac.Bly
{
    public class BlyAcquisitionCard : AcquisitionCard 
    {
        private static readonly ILog Log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);

        private string Name => "BlyAcquisitionCard";

        public EventHandler CommandFailed;

        Socket socket;
        private IPEndPoint serverEP;

        NetworkStream networkStream;
        private BackgroundWorker bwReceiver;

        public override bool IsConnected => socket != null && socket.Connected;

        public BlyAcquisitionCard(IPAddress serverIp, int port)
        {
            serverEP = new IPEndPoint(serverIp, port);
        }
      
        bool IsConnectionManagerWorking { get; set; }
        bool ShouldConnectionManagerContinueWorking { get; set; }
        Thread ConnectionManagerThread { get; set; }

        public override void KeepConnection()
        {
            if (IsConnectionManagerWorking) return;

            ConnectionManagerThread = new Thread(ManageConnectionLoop)
            {
                Name = $"{Name}_ConnectionManagerThread",
            };

            ShouldConnectionManagerContinueWorking = true;
            ConnectionManagerThread.Start();
        }

        private void ManageConnectionLoop()
        {
            const int nextTryDelaySeconds = 20;

            if (IsConnectionManagerWorking)
            {
                return;
            }

            IsConnectionManagerWorking = true;
            Debug.WriteLine($"[{Thread.CurrentThread.Name}] started working.");

            try
            {
                while (ShouldConnectionManagerContinueWorking)
                {
                    if (!IsConnected)
                    {
                        try
                        {
                            ConnectToServer();
                        }
                        catch (Exception e)
                        {
                            Log.Error(e.Message, e);
                        }
                    }

                    for (int i = 0; i < nextTryDelaySeconds; i++)
                    {
                        Thread.Sleep(1000);
                        if (!ShouldConnectionManagerContinueWorking)
                        {
                            return;
                        }
                    }
                }
            }
            finally
            {
                IsConnectionManagerWorking = false;
                Debug.WriteLine($"[{Thread.CurrentThread.Name}] stopped working.");
            }
        }


        private void ConnectToServer()
        {
            var connectorThread = new BackgroundWorker();
            connectorThread.DoWork += Connect_DoWork;
            connectorThread.RunWorkerCompleted += OnConnectWorkCompleted;
            connectorThread.RunWorkerAsync();
        }

        private void Connect_DoWork(object sender, DoWorkEventArgs e)
        {
            Debug.WriteLine($"{Thread.CurrentThread.Name}: {Name} connector background thread is going to work.");

            try
            {
                socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
                IAsyncResult result = socket.BeginConnect(serverEP, null, null);

                // 5 seconds are long enough for a local server connection.
                bool success = result.AsyncWaitHandle.WaitOne(5000, true);

                if (socket.Connected)
                {
                    socket.EndConnect(result);
                }
                else
                {
                    socket.Close();
                    throw new ApplicationException("Failed to connect server.");
                }

                e.Result = true;
                networkStream = new NetworkStream(socket);
                bwReceiver = new BackgroundWorker();
                bwReceiver.WorkerSupportsCancellation = true;
                bwReceiver.DoWork += StartReceive;
                bwReceiver.RunWorkerAsync();
            }
            catch (Exception ex)
            {
                e.Result = false;
                Log.Error("Connection failed.", ex);
            }

            Debug.WriteLine($"{Thread.CurrentThread.Name}: {Name} connector background thread work done.");
        }

        private bool CloseSocket()
        {
            if (socket != null && socket.Connected)
            {
                try
                {
                    bwReceiver.DoWork -= StartReceive;
                    socket.Shutdown(SocketShutdown.Both);

                    Thread.Sleep(10);

                    socket.Close();
                    bwReceiver.CancelAsync();
                }
                catch
                {
                    return false;
                }
            }

            return true;
        }

        public override bool Disconnect()
        {
            if (IsConnectionManagerWorking)
            {
                ShouldConnectionManagerContinueWorking = false;

                ConnectionManagerThread.Join();
            }

            return CloseSocket();
        }

        public override bool StartAcquisition()
        {
            const string commandStr = "[E]>ST0#";
            Debug.WriteLine($"{Name}: Start Acquisition.");
            SendCommand(commandStr);

            return true;
        }

        public override bool StopAcquisition()
        {
            const string commandStr = "[E]>ST1#";
            Debug.WriteLine($"{Name}: Stop Acquisition.");
            SendCommand(commandStr);

            return true;
        }

        public override bool InitParams(int samplingDepth, int cumulativePulses, int channelId = 0)
        {
            var commandStr = $"[E]>PU{channelId}|{samplingDepth}|{cumulativePulses}|20#";
            Debug.WriteLine($"{Name}: Set params samplingDepth=[{samplingDepth}], cumulativePulses=[{cumulativePulses}].");
            SendCommand(commandStr);

            commandStr = "[E]!SV#";
            Debug.WriteLine($"{Name}: Flash params.");
            SendCommand(commandStr);

            return true;
        }

        public override bool Reset()
        {
            var commandStr = "[E]!RS#";
            Debug.WriteLine($"{Name}: Reset card.");
            SendCommand(commandStr);

            return true;
        }

        private void SendCommand(Command cmd)
        {
            if (socket != null && socket.Connected)
            {
                var bwSender = new BackgroundWorker();
                bwSender.DoWork += bwSender_DoWork;
                bwSender.WorkerSupportsCancellation = true;
                bwSender.RunWorkerAsync(cmd);
            }
            else
            {
                OnCommandFailed(new EventArgs());
            }
        }

        public void SendCommand(string commandStr)
        {
            var buffer = new byte[commandStr.Length];
            for (var i = 0; i < commandStr.Length; i++)
            {
                buffer[i] = new ASCIIEncoding().GetBytes(commandStr.Substring(i, 1))[0];
            }

            var command = new Command(buffer);
            SendCommand(command);
        }

        private void bwSender_DoWork(object sender, DoWorkEventArgs e)
        {
            var cmd = (Command)e.Argument;
            e.Result = SendCommandToServer(cmd);
        }

        readonly Semaphore semaphor = new Semaphore(1, 1);
        private bool SendCommandToServer(Command cmd)
        {
            try
            {
                semaphor.WaitOne();

                if (networkStream.CanWrite)
                {
                    networkStream.Write(cmd.CmdInBytes, 0, cmd.LengthInBytes);
                    networkStream.Flush();

                    Debug.WriteLine($"{Name}: {cmd.CommandType} send.");
                    return true;
                }

                Debug.WriteLine($"{Name}: {cmd.CommandType} send failed, cannot write stream.");
                return false;
            }
            catch (Exception e)
            {
                Debug.WriteLine($"{Name}: {cmd.CommandType} send failed, {e.Message}.");
                return false;
            }
            finally
            {
                semaphor.Release();
            }
        }

        protected virtual void OnDisconnectedFromServer(object sender, EventArgs e)
        {
            Debug.WriteLine($"{Name}: disconnected From Server");

            AcquisitionCardStatusChanged?.Invoke(this, new AcquisitionCardStatusChangedEventArgs(false, "主动断开服务"));
        }

        protected virtual void OnCommandFailed(EventArgs e)
        {
            Debug.WriteLine("Command Failed");

            CommandFailed?.Invoke(this, e);
        }

        private void OnConnectWorkCompleted(object sender, RunWorkerCompletedEventArgs e)
        {
            if (!(bool)e.Result)
            {
                Debug.WriteLine($"{Name}: connection failed");
                AcquisitionCardStatusChanged?.Invoke(this, new AcquisitionCardStatusChangedEventArgs(false, "服务连接失败"));
            }
            else
            {
                Debug.WriteLine($"{Name}: connection Successed");
                AcquisitionCardStatusChanged?.Invoke(this, new AcquisitionCardStatusChangedEventArgs(true, "服务连接成功"));
            }

            ((BackgroundWorker)sender).Dispose();
        }

        private void StartReceive(object sender, DoWorkEventArgs e)
        {
            try
            {
                while (socket.Connected)
                {
                    Command command = ReadCommand();

                    if (null != command && command.CommandType == CommandType.UpDataAck)
                    {
                        ProcessData(command.CmdInBytes);
                    }
                }
            }
            catch (Exception ex)
            {
                Log.Error($"{Name} packet error.", ex);
            }

            OnDisconnectedFromServer(this, new EventArgs());
            CloseSocket();
        }

        private void ProcessData(byte[] bytes)
        {
            var args = BlyValueConverter.Convert(bytes);

            OnLidarDataRecieved(this, args);
        }

        private Command ReadCommand()
        {
            var first = new byte[1];
            _ = Read(first, 0, first.Length);

            var type = CommandParas.GetCommandType(first[0]);
            var packet = new List<byte>();
            packet.AddRange(first);

            switch (type)
            {
                case CommandType.UpDataAck:
                    {
                        var head = new byte[10];
                        Read(head, 0, head.Length);
                        var packetLength = BitConverter.ToUInt16(head, 8);
                        var dataLength = packetLength - 11;
                        var data = new byte[dataLength];
                        Read(data, 0, data.Length);

                        packet.AddRange(head);
                        packet.AddRange(data);

                        var temp = new List<byte>();
                        if (packet[packetLength - 1] != 0x55)
                        {
                            do
                            {
                                Read(first, 0, first.Length);
                                temp.AddRange(first);

                            } while (first[0] != 0x55);
                        }

                        return new Command(packet.ToArray());
                    }
                case CommandType.Up0xCF:
                case CommandType.Up0xA0:
                case CommandType.Up0xAF:
                    {
                        var tail = new byte[14];
                        Read(tail, 0, tail.Length);
                        packet.AddRange(tail);

                        if (type != CommandType.Up0xCF)
                        {
                            Debug.WriteLine($"{Name}: {PacketUtils.ToHexString(packet)}");
                        }

                        return new Command(packet.ToArray());
                    }
                case CommandType.UpUnknown:
                default:
                    {
                        Debug.WriteLine($"{Name}: Unknown packet received!");
                        return null;
                    }
            }
        }

        private bool Read(byte[] buf, int offset, int bytesWanted)
        {
            if (buf == null || buf.Length < offset + bytesWanted)
            {
                throw new ArgumentOutOfRangeException();
            }

            var alreadyRead = offset;
            var bytesToRead = bytesWanted;

            try
            {
                while (networkStream.CanRead)
                {
                    var bytesRead = networkStream.Read(buf, alreadyRead, bytesToRead);

                    if (bytesRead == 0)
                    {
                        return false;
                    }
                    alreadyRead += bytesRead;

                    if (alreadyRead >= bytesWanted)
                    {
                        break;
                    }
                    bytesToRead -= bytesRead;
                }

                return true;
            }
            catch (Exception ex)
            {
                Debug.WriteLine($"{Name}: read exception, message=[{ex.Message}].");
            }

            return false;
        }
    }
}
